package project.parser.tokenizer;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;

public class Tokenizer implements Iterator<TokenType> {
	// symbol table mapping keywords and symbols to their token types
	private final static Map<String,TokenType> symbolTable = new HashMap<>();
	/*
	 * regular expression recognizing all valid tokens, including white spaces
	 * matches the end of the line if no valid token is found
	 */
	private final static StringBuilder globalPattern = new StringBuilder();
	// matcher associated with a reader which tries to match tokens with
	// globalPatterns
	final private ReaderMatcher matcher;
	// the type of the token, if matched
	private TokenType tokenType;
	// the string of the token, if matched
	private String tokenString;
	// the integer value of the token, if the token type is NUM
	private int intValue;
	// the Boolean value of the token, if the token type is BOOL
	private boolean boolValue;

	private static void appendOr(StringBuilder sb, CharSequence st) {
		sb.append(st);
		sb.append("|");
	}

	static {
		StringBuilder keywordPatterns = new StringBuilder("(");
		for (TokenType tt : TokenType.values()) {
			String pat = tt.getPattern();
			if (tt.isKeyword())
				appendOr(keywordPatterns, pat);
			else if (pat != null)
				appendOr(globalPattern, pat);
			String token = tt.getToken();
			if (token != null)
				symbolTable.put(token, tt);
		}
		keywordPatterns.append(TokenType.IDENT_HEAD);
		keywordPatterns.append(")");
		keywordPatterns.append(TokenType.IDENT_TAIL);
		globalPattern.append(keywordPatterns);
	}

	public Tokenizer(Reader reader) {
		matcher = new ReaderMatcher(globalPattern.toString(), reader);
	}

	// returns the type of the matched token
	private TokenType type(String st) {
		if (matcher.group(TokenType.SKIP_GROUP) != null)
			return TokenType.SKIP; // white spaces
		if (matcher.group(TokenType.DEC_NUM_GROUP) != null) {
			intValue = Integer.parseInt(st);
			return tokenType = TokenType.NUM;
		}
		if (matcher.group(TokenType.BIN_NUM_GROUP) != null) {
			intValue = Integer.parseInt(matcher.group(TokenType.BIN_NUM_GROUP), 2);
			return tokenType = TokenType.NUM;
		}
		if (matcher.group(TokenType.IDENT_HEAD_GROUP) != null
				|| matcher.group(TokenType.IDENT_TAIL_GROUP) != null
				&& matcher.group(TokenType.IDENT_TAIL_GROUP).length() > 0)
			return tokenType = TokenType.IDENT;
		// the token must necessarily be in the symbol table
		tokenType = symbolTable.get(st);
		assert tokenType != null;
		if (TokenType.BOOL_GROUP.contains(tokenType)) {
			tokenType = TokenType.BOOL;
			boolValue = Boolean.parseBoolean(st);
		}
		return tokenType;
	}

	@Override
	public boolean hasNext() {
		return tokenType != TokenType.EOF;
	}

	@Override
	public TokenType next() {
		if (!hasNext())
			throw new NoSuchElementException();
		try {
			while (matcher.hasNext()) {
				if (matcher.nextMatches()) {
					tokenString = matcher.group();
					tokenType = type(tokenString);
					if (tokenType != TokenType.SKIP)
						return tokenType;
				} else {
					String unknownToken = matcher.getSkippedString();
					tokenType = null;
					throw new UnknownTokenException(unknownToken);
				}
			}
		} catch (IOException e) {
			tokenType = null;
			throw new IOTokenException(e.getMessage(), e);
		}
		return tokenType = TokenType.EOF;
	}

	private void checkTokenType() {
		if (tokenType == null)
			throw new IllegalStateException("No valid token");
	}

	private void checkTokenType(TokenType tt) {
		if (tokenType != tt)
			throw new IllegalStateException("No valid token of type "
					+ tt.name());
	}

	public TokenType tokenType() {
		checkTokenType();
		return tokenType;
	}

	public String tokenString() {
		checkTokenType();
		return tokenString;
	}

	public int intValue() {
		checkTokenType(TokenType.NUM);
		return intValue;
	}

	public boolean boolValue() {
		checkTokenType(TokenType.BOOL);
		return boolValue;
	}

	public static void main(String[] args) {
		Tokenizer tk = new Tokenizer(new InputStreamReader(System.in));
		TokenType tt = null;
		while (tk.hasNext()) {
			try {
				tt = tk.next();
				System.out.println(tt
						+ " "
						+ (tt == TokenType.NUM ? tk.intValue() : (tt == TokenType.BOOL ? tk
						.boolValue() : tt == TokenType.IDENT ? tk.tokenString()
						: "")));
			} catch (Exception e) {
				System.out.println(e.getMessage());
			}
		}
	}
}
